<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JWT + 双Token 认证演示</title>
    <!-- 引入 LayUI 样式 -->
    <link rel="stylesheet" href="./layui-dist/css/layui.css">
    <style>
        body {
            background-color: #f2f2f2;
            padding: 20px;
        }
        .container {
            max-width: 1200px;
            margin: 0 auto;
        }
        .header {
            text-align: center;
            margin-bottom: 20px;
            padding: 20px;
            background-color: white;
            border-radius: 5px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
        }
        .card {
            background-color: white;
            border-radius: 5px;
            padding: 20px;
            margin-bottom: 20px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
        }
        .token-display {
            background-color: #f8f8f8;
            border: 1px solid #eaeaea;
            border-radius: 4px;
            padding: 15px;
            margin-top: 15px;
            font-family: monospace;
            font-size: 12px;
            word-break: break-all;
            white-space: pre-wrap;
            max-height: 200px;
            overflow-y: auto;
        }
        .status {
            padding: 10px;
            border-radius: 4px;
            margin-top: 10px;
        }
        .status-success {
            background-color: #f0f9eb;
            color: #67c23a;
            border: 1px solid #e1f3d8;
        }
        .status-error {
            background-color: #fef0f0;
            color: #f56c6c;
            border: 1px solid #fbc4c4;
        }
        .status-info {
            background-color: #f4f4f5;
            color: #909399;
            border: 1px solid #e9e9eb;
        }
        .code-block {
            background-color: #f5f5f5;
            border-left: 4px solid #409EFF;
            padding: 10px 15px;
            margin: 10px 0;
            font-family: monospace;
            font-size: 13px;
            line-height: 1.5;
        }
        .api-test-buttons {
            margin-bottom: 15px;
        }
        .api-test-buttons .layui-btn {
            margin-right: 10px;
            margin-bottom: 10px;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>JWT + 双Token 认证系统演示</h1>
            <p class="layui-word-aux">使用Spring Boot + LayUI + jQuery实现</p>
        </div>

        <div class="layui-row layui-col-space15">
            <!-- 左侧列 -->
            <div class="layui-col-md6">
                <!-- 登录面板 -->
                <div class="card">
                    <h2>用户登录</h2>
                    <form class="layui-form" id="login-form">
                        <div class="layui-form-item">
                            <label class="layui-form-label">用户名</label>
                            <div class="layui-input-block">
                                <input type="text" name="username" required lay-verify="required" placeholder="请输入用户名" autocomplete="off" class="layui-input" value="admin">
                            </div>
                        </div>
                        <div class="layui-form-item">
                            <label class="layui-form-label">密码</label>
                            <div class="layui-input-block">
                                <input type="password" name="password" required lay-verify="required" placeholder="请输入密码" autocomplete="off" class="layui-input" value="123456">
                            </div>
                        </div>
                        <div class="layui-form-item">
                            <div class="layui-input-block">
                                <button class="layui-btn" lay-submit lay-filter="login-form">登录</button>
                                <button type="reset" class="layui-btn layui-btn-primary">重置</button>
                            </div>
                        </div>
                    </form>
                </div>
                
                <!-- Token信息面板 -->
                <div class="card">
                    <h2>Token 信息</h2>
                    <div class="layui-form-item">
                        <label class="layui-form-label">Access Token</label>
                        <div class="layui-input-block">
                            <textarea id="access-token" class="layui-textarea token-display" readonly placeholder="登录后显示Access Token"></textarea>
                        </div>
                    </div>
                    <div class="layui-form-item">
                        <label class="layui-form-label">Refresh Token</label>
                        <div class="layui-input-block">
                            <textarea id="refresh-token" class="layui-textarea token-display" readonly placeholder="登录后显示Refresh Token"></textarea>
                        </div>
                    </div>
                    <div class="layui-form-item">
                        <div class="layui-input-block">
                            <button class="layui-btn layui-btn-normal" id="refresh-token-btn">刷新Token</button>
                            <button class="layui-btn layui-btn-danger" id="logout-btn">退出登录</button>
                        </div>
                    </div>
                </div>
            </div>
            
            <!-- 右侧列 -->
            <div class="layui-col-md6">
                <!-- API测试面板 -->
                <div class="card">
                    <h2>API 测试</h2>
                    <div class="api-test-buttons">
                        <button class="layui-btn" id="public-api-btn">访问公开API</button>
                        <button class="layui-btn layui-btn-warm" id="protected-api-btn">访问受保护API</button>
                        <button class="layui-btn layui-btn-normal" id="user-info-btn">获取用户信息</button>
                    </div>
                    <div class="layui-form-item">
                        <label class="layui-form-label">API响应</label>
                        <div class="layui-input-block">
                            <textarea id="api-response" class="layui-textarea token-display" readonly placeholder="API响应将显示在这里" style="min-height: 200px;"></textarea>
                        </div>
                    </div>
                </div>
                
                <!-- 系统状态面板 -->
                <div class="card">
                    <h2>系统状态</h2>
                    <div id="system-status">
                        <div class="status status-info">
                            <i class="layui-icon layui-icon-tips"></i> 未登录
                        </div>
                    </div>
                </div>
                
                <!-- 工作原理面板 -->
                <div class="card">
                    <h2>工作原理</h2>
                    <div class="code-block">
// 1. 登录获取双Token
POST /api/auth/login
{
  "username": "admin",
  "password": "123456"
}

// 响应
{
  "success": true,
  "message": "登录成功",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "expiresIn": 3600
  }
}
                    </div>
                    <div class="code-block">
// 2. 使用Access Token访问受保护API
GET /api/protected/resource
Authorization: Bearer {accessToken}

// 3. Access Token过期后使用Refresh Token刷新
POST /api/auth/refresh
{
  "refreshToken": "{refreshToken}"
}
                    </div>
                </div>
            </div>
        </div>
    </div>

    <!-- 引入 LayUI 和 jQuery -->
    <script src="./layui-dist/layui.js"></script>
    <script src="./jquery-3.7.1.min.js"></script>
    <script>
        // 使用 LayUI 的模块
        layui.use(['form', 'layer'], function(){
            var form = layui.form;
            var layer = layui.layer;
            
            // 基础URL - 根据实际部署情况修改
            const BASE_URL = 'http://localhost:6100';
            
            // 存储Token
            let accessToken = localStorage.getItem('accessToken');
            let refreshToken = localStorage.getItem('refreshToken');
            // 倒计时计时器句柄
            let countdownTimer = null;
            // 刷新相关配置与状态
            const REFRESH_THRESHOLD_SECONDS = 4; // 剩余 <= 4 秒时，提前刷新
            let isRefreshing = false;
            // 刷新回调等待队列（避免并发多次刷新）
            let refreshSuccessQueue = [];
            let refreshErrorQueue = [];
            // 主动刷新去重标记（随每次渲染的 exp 重新初始化）
            let proactiveRefreshTriggered = false;
            
            // 初始化状态
            updateSystemStatus();
            
            // 登录表单提交
            form.on('submit(login-form)', function(data){
                $.ajax({
                    url: BASE_URL + '/api/auth/login',
                    type: 'POST',
                    contentType: 'application/json',
                    data: JSON.stringify(data.field),
                    success: function(response) {
                        if (response.code==200) {
                            accessToken = response.data.accessToken;
                            refreshToken = response.data.refreshToken;
                            
                            // 保存到localStorage
                            localStorage.setItem('accessToken', accessToken);
                            localStorage.setItem('refreshToken', refreshToken);
                            
                            // 更新UI
                            $('#access-token').text(accessToken);
                            $('#refresh-token').text(refreshToken);
                            updateSystemStatus();
                            
                            layer.msg('登录成功', {icon: 1});
                        } else {
                            layer.msg('登录失败: ' + response.message, {icon: 2});
                            console.log(response);
                        }
                    },
                    error: function(xhr) {
                        layer.msg('登录请求失败: ' + xhr.status + ' ' + xhr.statusText, {icon: 2});
                    }
                });
                return false; // 阻止表单跳转
            });
            
            // 刷新Token
            $('#refresh-token-btn').click(function() {
                if (!refreshToken) {
                    layer.msg('请先登录', {icon: 2});
                    return;
                }
                // 复用统一刷新逻辑（带锁与队列）
                refreshTokens(function onOk(){
                    layer.msg('Token刷新成功', {icon: 1});
                }, function onErr(){
                    layer.msg('Token刷新失败，请重新登录', {icon: 2});
                });
            });
            
            // 退出登录
            $('#logout-btn').click(function() {
                logout();
                layer.msg('已退出登录', {icon: 1});
            });
            
            // 访问公开API
            $('#public-api-btn').click(function() {
                $.ajax({
                    url: BASE_URL + '/api/public/info',
                    type: 'GET',
                    success: function(response) {
                        $('#api-response').text(JSON.stringify(response, null, 2));
                    },
                    error: function(xhr) {
                        $('#api-response').text('请求失败: ' + xhr.status + ' ' + xhr.statusText + '\n' + xhr.responseText);
                    }
                });
            });
            
            // 访问受保护API
            $('#protected-api-btn').click(function() {
                if (!accessToken) {
                    layer.msg('请先登录', {icon: 2});
                    return;
                }
                
                callProtectedAPI(BASE_URL + '/api/protected/resource');
            });
            
            // 获取用户信息
            $('#user-info-btn').click(function() {
                if (!accessToken) {
                    layer.msg('请先登录', {icon: 2});
                    return;
                }
                
                callProtectedAPI(BASE_URL + '/api/protected/user-info');
            });
            
            // 调用受保护API的通用方法
            function callProtectedAPI(url) {
                // 在发送前确保 Access Token 有效或已提前刷新（回调写法）
                ensureAccessTokenValid(function onOk() {
                    // 第一次尝试
                    $.ajax({
                        url: url,
                        type: 'GET',
                        beforeSend: function(xhr) {
                            console.log("Access Token: ", accessToken);
                            xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken);
                        },
                        success: function(response) {
                            $('#api-response').text(JSON.stringify(response, null, 2));
                        },
                        error: function(xhr) {
                            if (xhr.status === 401) {
                                // 401 后尝试刷新并重放一次（回调写法）
                                refreshTokens(function onRefreshOk() {
                                    $.ajax({
                                        url: url,
                                        type: 'GET',
                                        beforeSend: function(xhr2) {
                                            xhr2.setRequestHeader('Authorization', 'Bearer ' + accessToken);
                                        },
                                        success: function(resp2) {
                                            $('#api-response').text(JSON.stringify(resp2, null, 2));
                                        },
                                        error: function(xhr2) {
                                            layer.msg('请求失败: ' + xhr2.status + ' ' + xhr2.statusText, {icon: 2});
                                            $('#api-response').text('请求失败: ' + xhr2.status + ' ' + xhr2.statusText + '\n' + xhr2.responseText);
                                        }
                                    });
                                }, function onRefreshErr() {
                                    layer.msg('自动刷新失败，请重新登录', {icon: 2});
                                });
                            } else {
                                console.log(xhr);
                                $('#api-response').text('请求失败: ' + xhr.status + ' ' + xhr.statusText + '\n' + xhr.responseText);
                            }
                        }
                    });
                }, function onFail() {
                    layer.msg('Token无效或刷新失败，请重新登录', {icon: 2});
                });
            }
            
            // 解析 Access Token 过期时间（秒）
            function parseAccessTokenExp() {
                if (!accessToken) return null;
                try {
                    const payload = JSON.parse(atob(accessToken.split('.')[1]));
                    return typeof payload.exp === 'number' ? payload.exp : null;
                } catch (e) {
                    return null;
                }
            }

            // 确保 Access Token 在阈值内有效，如将过期则先刷新
            function ensureAccessTokenValid(onOk, onFail) {
                const expSec = parseAccessTokenExp();
                if (!expSec) {
                    if (typeof onFail === 'function') onFail();
                    return;
                }
                const nowSec = Math.floor(Date.now() / 1000);
                const timeLeft = expSec - nowSec;
                if (timeLeft > REFRESH_THRESHOLD_SECONDS) {
                    if (typeof onOk === 'function') onOk();
                    return;
                }
                // 进入提前刷新
                refreshTokens(onOk, onFail);
            }

            // 刷新 Access/Refresh Token（带并发锁）
            function refreshTokens(onSuccess, onError) {
                if (!refreshToken) {
                    if (typeof onError === 'function') onError();
                    return;
                }
                if (isRefreshing) {
                    if (typeof onSuccess === 'function') refreshSuccessQueue.push(onSuccess);
                    if (typeof onError === 'function') refreshErrorQueue.push(onError);
                    return;
                }
                isRefreshing = true;
                $.ajax({
                    url: BASE_URL + '/api/auth/refresh',
                    type: 'POST',
                    contentType: 'application/json',
                    data: JSON.stringify({ refreshToken: refreshToken }),
                    success: function(response) {
                        if (response && (response.success === true || response.code === 200)) {
                            accessToken = response.data.accessToken;
                            refreshToken = response.data.refreshToken;
                            localStorage.setItem('accessToken', accessToken);
                            localStorage.setItem('refreshToken', refreshToken);
                            $('#access-token').text(accessToken);
                            $('#refresh-token').text(refreshToken);
                            updateSystemStatus();
                            if (typeof onSuccess === 'function') onSuccess();
                            while (refreshSuccessQueue.length) {
                                try { refreshSuccessQueue.shift()(); } catch (e) {}
                            }
                            refreshErrorQueue = [];
                        } else {
                            logout();
                            if (typeof onError === 'function') onError();
                            while (refreshErrorQueue.length) {
                                try { refreshErrorQueue.shift()(); } catch (e) {}
                            }
                            refreshSuccessQueue = [];
                        }
                    },
                    error: function() {
                        logout();
                        if (typeof onError === 'function') onError();
                        while (refreshErrorQueue.length) {
                            try { refreshErrorQueue.shift()(); } catch (e) {}
                        }
                        refreshSuccessQueue = [];
                    },
                    complete: function() {
                        isRefreshing = false;
                    }
                });
            }

            // 尝试刷新Token
            function attemptTokenRefresh() {
                if (!refreshToken) {
                    layer.msg('Refresh Token不存在，请重新登录', {icon: 2});
                    return;
                }
                
                $.ajax({
                    url: BASE_URL + '/api/auth/refresh',
                    type: 'POST',
                    contentType: 'application/json',
                    data: JSON.stringify({refreshToken: refreshToken}),
                    success: function(response) {
                        if (response.success) {
                            accessToken = response.data.accessToken;
                            refreshToken = response.data.refreshToken;
                            
                            localStorage.setItem('accessToken', accessToken);
                            localStorage.setItem('refreshToken', refreshToken);
                            
                            $('#access-token').text(accessToken);
                            $('#refresh-token').text(refreshToken);
                            updateSystemStatus();
                            
                            layer.msg('Token已自动刷新，请重试操作', {icon: 1});
                        } else {
                            layer.msg('自动刷新失败: ' + response.message, {icon: 2});
                            logout();
                        }
                    },
                    error: function(xhr) {
                        layer.msg('自动刷新请求失败', {icon: 2});
                        logout();
                    }
                });
            }
            
            // 退出登录函数
            function logout() {
                accessToken = null;
                refreshToken = null;
                
                // 清除localStorage
                localStorage.removeItem('accessToken');
                localStorage.removeItem('refreshToken');
                
                // 更新UI
                $('#access-token').text('');
                $('#refresh-token').text('');
                $('#api-response').text('');
                updateSystemStatus();
            }
            
            // 更新系统状态
            function updateSystemStatus() {
                const statusDiv = $('#system-status');
                statusDiv.empty();
                // 清理上一次的倒计时
                if (countdownTimer) {
                    clearInterval(countdownTimer);
                    countdownTimer = null;
                }
                
                if (accessToken) {
                    statusDiv.append('<div class="status status-success"><i class="layui-icon layui-icon-ok"></i> 已登录，Token有效</div>');
                    
                    // 显示Token过期时间
                    try {
                        const payload = JSON.parse(atob(accessToken.split('.')[1]));
                        const exp = new Date(payload.exp * 1000);
                        const now = new Date();
                        const timeLeft = Math.floor((exp - now) / 1000);
                        
                        statusDiv.append('<div class="status status-info">Token过期时间: ' + exp.toLocaleString() + '</div>');
                        statusDiv.append('<div id="token-time-left" class="status status-info">剩余时间: ' + Math.max(0, timeLeft) + '秒</div>');

                        // 启动倒计时
                        proactiveRefreshTriggered = false;
                        const updateCountdown = function() {
                            const nowTick = new Date();
                            const secondsLeft = Math.max(0, Math.floor((exp - nowTick) / 1000));
                            $('#token-time-left').text('剩余时间: ' + secondsLeft + '秒');

                            // 当接近过期时，主动刷新（仅触发一次，且不与并发刷新冲突）
                            if (!proactiveRefreshTriggered && secondsLeft <= REFRESH_THRESHOLD_SECONDS && secondsLeft > 0) {
                                if (!isRefreshing) {
                                    proactiveRefreshTriggered = true;
                                    refreshTokens(function(){}, function(){});
                                }
                            }
                            if (secondsLeft <= 0) {
                                clearInterval(countdownTimer);
                                countdownTimer = null;
                            }
                        };
                        updateCountdown();
                        countdownTimer = setInterval(updateCountdown, 1000);
                    } catch (e) {
                        console.error('解析JWT失败:', e);
                    }
                } else {
                    statusDiv.append('<div class="status status-info"><i class="layui-icon layui-icon-tips"></i> 未登录</div>');
                }
            }
            
            // 如果已保存Token，则加载到UI
            if (accessToken) {
                $('#access-token').text(accessToken);
                $('#refresh-token').text(refreshToken);
            }
        });
    </script>
</body>
</html>